
const modelCreator = options => {
  const {
    model: model_param,
    rpc: rpc_param,
    env: env_param
  } = options;

  class cls {
    constructor(ids) {
      this._ids = [];
      this._id = null;

      if (ids && typeof ids === 'object') {
        this._ids = ids;
        this._id = ids.length ? ids[0] : null;
      } else {
        this._id = ids;
        this._ids = ids ? [ids] : [];
      }
    }

    get length() {
      return this._ids.length;
    }

    get ids() {
      return this._ids;
    }

    get id() {
      return this._id;
    }

    async call( method, args=[], kwargs={} ) {
      return cls.call( method, [this._ids, ...args], kwargs );
    }

    // TBD, if after call setAttr, then we have custom field.
    //  but look dont return custom field
    look(fields) {
      return this.look1(fields);
    }

    look1(fields) {
      if (this._id) {
        return cls._get_one(this._id, fields);
      } else {
        return {};
      }
    }

    look2(fields) {
      return cls._get_multi(this._ids, fields);
    }


    // to del, 2017-7-17, few to use
    /*
    // only for single. //TBD , check typeof( value )
    setAttr(attr, value) {
      // only for single
      const rec = cls._records[this._id];

      const {
        type,
        //relation ,
      } = cls._fields[attr] || {};

      if (['many2one', 'one2many', 'many2many'].indexOf(type) < 0) {
        rec[attr] = value;
      } else if (type === 'many2one') {
        //TBD , check typeof( value )
        rec[attr] = value._id;
      } else {
        //TBD , check typeof( value )
        rec[attr] = value._ids;
      }
    }

    setattr(attr, value){
      return this.setAttr(attr, value)
    }

    attr(attr, ref = 0, ref_fields = {}) {
      // only for single
      if (ref) {
        return this._get_ref(attr, ref_fields);
      }

      const raw = (cls._records[this._id] || {})[attr];
      const { type, relation } = cls._fields[attr] || {};
      if (['many2one', 'one2many', 'many2many'].indexOf(type) < 0) {
        return raw;
      } else {
        const ref_cls = cls._get_model(relation);
        return new ref_cls(raw);
      }
    }

    async _get_ref(attr, ref_fields) {
      const {
        relation: ref_cls_name,
        //type: ref_type,
      } = cls._fields[attr];
      const ref_cls = cls._get_model(ref_cls_name);
      const ref_ins = this.attr(attr);
      return await ref_cls.browse(ref_ins._ids, ref_fields);
    }

    */

  }

  Object.defineProperty(cls, 'name', { value: model_param, configurable: true });

  cls._name = model_param;
  cls._rpc = rpc_param;
  cls._env = env_param;
  cls._records = {};
  cls._context = {};
  cls._context2 = {};

  cls._extends = [];
  cls._sudo_user = {};

  cls._fields = {
    id: { type: 'integer' },
    display_name: { type: 'char' },
    name: { type: 'char' },
  };

  cls._config = {
    init: false
  };

  cls.fields = cls._fields;

  cls.with_context = (context) => {
    cls._context = { ...cls._context, ...context };
    return cls._env[cls._name];
  };

  cls.with_context2 = (context) => {
    cls._context2 = { ...cls._context2, ...context };
    return cls._env[cls._name];
  };

  cls.env = (model) =>{
    return cls._get_model(model)
  }

  cls._get_model = model => {
    let new_cls = cls._env[model];
    if (!new_cls) {
      new_cls = modelCreator({ model, rpc: cls._rpc, env: cls._env });
      cls._env[model] = new_cls;
    }
    return new_cls;

  };

  // to check, 2019-7-17, error.     never used?
  cls.sudo = user => {
    const new_cls0 = cls._sudo_user[user];
    if (new_cls0) {
      return new_cls0;
    }

    const new_cls = modelCreator(options);
    new_cls._fields = cls._fields;
    new_cls._records = cls._records;
    new_cls._extends = cls._extends;
    new_cls._sudo_user = cls._sudo_user;
    new_cls._sudo = user;

    let extend_new_cls = new_cls;
    for (const extend of cls._extends) {
      extend_new_cls = extend(extend_new_cls);
    }
    Object.defineProperty(extend_new_cls, 'name', {
      value: cls._name,
      configurable: true,
    });
    cls._sudo_user[user] = extend_new_cls;
    return extend_new_cls;
  };

  cls.rawcall = async ( method, args=[], kwargs ={}, success_callback, error_callback ) => {
    const data = await cls.call(method, args, kwargs, success_callback, error_callback);
    const { return_with_error } = cls._context2;
    return return_with_error ? data.result : data;
  };

  cls.call = async ( method, args=[], kwargs ={}, success_callback, error_callback ) => {
    const params = {
      model: cls._name,
      method,
      args,
      kwargs,
      sudo: cls._sudo,
    };

    const { return_with_error } = cls._context2;

    const data = await cls._rpc.call(params);

    const { code, result, error } = data;

    if( return_with_error ){
      if (!code) {
        return {
          code,
          result:  success_callback ? success_callback( result ) : result,
        }
      } else {
        return {
          code,
          result: error_callback ? error_callback( error ) : null,
          error,
        }
      }
    }
    else {
      if (!code) {
        return success_callback ? success_callback( result ) : result;
      } else {
        return error_callback ? error_callback( error ) : null;
      }
    }
  };

  cls._init_fields_to_call = async fields => {
      const metas0 = await cls.rawcall('fields_get2', [fields])
      const metas = metas0 || {}

      Object.keys(metas).forEach( model => {
        const new_cls = cls._get_model(model)
        new_cls._config.init = true;

        const to_fields = new_cls._fields
        const from_fields = metas[model]

        for (const fld in from_fields) {
          to_fields[fld] = from_fields[fld];
        }

      })

  }

  cls._init_fields_check = fields => {
    if (!cls._config.init) {
      return true
    }

    // fields must be dict
    const fields2 = fields || {}

    for ( const fld in fields2 ) {
      const { type, relation } = cls._fields[fld];
      if (['many2one', 'one2many', 'many2many'].indexOf(type) < 0) {
        continue
      }

      const ref_cls = cls._get_model(relation);
      const ref_check = ref_cls._init_fields_check( fields2[fld] )
      if (ref_check){
        return true;
      }
    }

    return false;
  }

  cls._init_fields = async fields => {
    // TBD, 2019-7-19, to transfer fields from list to dict
    const check_ret = cls._init_fields_check(fields)
    if(check_ret){
      await cls._init_fields_to_call(fields)
    }

  }

  cls._set_multi = (data, fields = {}) => {
    // TBD, 2019-7-19, to transfer fields from list to dict
    const ids = data.reduce((acc, cur) => {
      const id = cls._set_one(cur, fields);
      if (id) {
        // TBD
      }
      acc.push(cur.id);
      return acc;
    }, []);
    return ids;
  };

  cls._set_one = (data, fields = {}) => {
    const { id } = data;
    if (!id) {
      return id;
    }

    const vals = Object.keys(data).reduce((acc, fld) => {
      const value = data[fld];
      const { type, relation } = (cls._fields || {})[fld] || {};

      if (['many2one', 'one2many', 'many2many'].indexOf(type) < 0) {
        if (type === 'date') {
          acc[fld] = value || null;
        } else {
          acc[fld] = value;
        }

        // TBD , bin , image ?
        return acc;
      }

      const ref_cls = cls._get_model(relation);
      if (type === 'many2one') {
        if (!value) {
          acc[fld] = null;
        } else if (value.length === 0) {
          acc[fld] = null;
        } else {
          // TBD: to set name, after to check cls._records

          const ref_vals = fields[fld]
            ? value[0]
            : {
                id: value[0],
                display_name: value[1],
              };

          const ref_rec = ref_cls._records[ref_vals.id];
          if (!fields[fld] && !(ref_rec && ref_rec.display_name)) {
            ref_vals['name'] = value[1];
          }

          ref_cls._set_one(ref_vals, fields[fld]);
          acc[fld] = ref_vals.id;
        }
      }
      else {
        if (fields[fld]) {
          ref_cls._set_multi(value, fields[fld]);
          acc[fld] = value.map(item => item.id);
        } else {
          acc[fld] = value;
        }
      }
      return acc;
    }, {});
    cls._records[id] = { ...(cls._records[id] || {}), ...vals };
    return id;
  };

  cls._get_one = (id, fields0, notall) => {
    const fields1 = Object.keys(cls._fields).reduce((acc, cur) => {
      acc[cur] = 1;
      return acc;
    }, {});

    const fields00 = fields0 || {};
    const fields = !notall ? { ...fields1, ...fields00 } : fields0 || fields1;

    const get_ref_fields = (fld, fields) => {
      let ref_fields = { name: null, display_name: null };
      if (fld in fields) {
        const ref_flds = fields[fld];
        if (typeof ref_flds === 'object') {
          ref_fields = ref_flds;
        }
      }

      return ref_fields;
    };

    return Object.keys(fields).reduce(
      (item, fld) => {
        const fld_meta = cls._fields[fld];
        if (!fld_meta) {
          return item;
        }

        const { type, relation } = fld_meta;

        if (['many2one', 'one2many', 'many2many'].indexOf(type) < 0) {
          if (cls._records[id]) {
            item[fld] = cls._records[id][fld];
          }
        } else if (type === 'many2one') {
          const ref_cls = cls._get_model(relation);

          const ref_id = cls._records[id] ? cls._records[id][fld] : null;
          const ref_fields = get_ref_fields(fld, fields);
          const noall = !(typeof fields[fld] === 'object');
          item[fld] = ref_id && ref_cls._get_one(ref_id, ref_fields, noall);
        } else {
          const ref_cls = cls._get_model(relation);
          const ref_ids = cls._records[id] ? cls._records[id][fld] : null;
          const ref_fields = get_ref_fields(fld, fields);
          const noall = !(typeof fields[fld] === 'object');
          item[fld] = ref_ids && ref_cls._get_multi(ref_ids, ref_fields, noall);
        }

        return item;
      },
      { id }
    );
  };

  cls._get_multi = (ids, fields, notall) => {
    return ids.reduce((records, id) => {
      const item = cls._get_one(id, fields, notall);
      records.push(item);
      return records;
    }, []);
  };

  cls.fields_get = async (allfields, attributes) => {
    const data = await cls.call('fields_get', [allfields, attributes]);
    const { return_with_error } = cls._context2;
    const result = return_with_error ? data.result || {} : data;

    const fields = result || {};

    if (!allfields) {
      return fields;
    }

    return Object.keys(fields).reduce((acc, cur) => {
      if (allfields.indexOf(cur) >= 0) {
        acc[cur] = fields[cur];
      }
      return acc;
    }, {});
  };

  cls.call_with_read = async ({ method, args, kwargs }, fields ) => {
    return cls.call(
      method, args, kwargs,
      (result)=>{
        const ids = cls._set_multi(result || [], fields);
        return cls.view(ids);
      },
      (error)=>{
        return cls.view([])
      }
    );
  }

  cls.call_as_create_read = async ({ method, args, kwargs={} }, fields ) => {
    await cls._init_fields(fields)
    const { context={} } = kwargs;

    return cls.call_with_read({
      method,
      args,
      kwargs: {
        ...kwargs,
        context: {
          ...context,
          create_read: fields,
        }
      },
    }, fields );

  }

  cls.call_as_write_read = async ({ method, args, kwargs={} }, fields ) => {
    await cls._init_fields(fields)
    const { context={} } = kwargs;

    return cls.call_with_read({
      method,
      args,
      kwargs: {
        ...kwargs,
        context: {
          ...context,
          write_read: fields,
        }
      },
    }, fields );

  }

  cls.search = async ( domain, fields={}, kwargs={} ) => {
    // { offset=null,limit=null,order=null, context: {return_with_error} } = kwargs
    await cls._init_fields(fields)

    return cls.call_with_read({
        method: 'search_read2',
        args: [domain, fields],
        kwargs,
    }, fields, );

  };

  cls.browse = async (ids, fields={}, kwargs={} ) => {
    await cls._init_fields(fields)
    return cls.call_with_read({
        method: 'read2',
        args: [ids, fields],
        kwargs,
    }, fields, );

  }

  cls.create = async ( vals, fields={}, kwargs={}) => {
    await cls._init_fields(fields)
    return cls.call_with_read({
        method: 'create2',
        args: [vals, fields],
        kwargs,
    }, fields );
  };

  cls.write = async (id, vals, fields={}, kwargs={}) => {
    await cls._init_fields(fields)
    return cls.call_with_read({
        method: 'write2',
        args: [id, vals, fields],
        kwargs,
    }, fields );
  };

  cls.unlink = async ( id, kwargs={} ) => {
    const data = await cls.call(
      'unlink',
      [id],
      kwargs,
      (result)=>{
        delete cls._records[id];
        return result;
      },
    );

    return data;

  };

  cls.toggle_active = async (id, fields={}, kwargs={}) => {
    return cls.call_as_write_read({
        method: 'toggle_active',
        args: [id],
        kwargs,
    }, fields)
  };

  cls.view = id => {
    const myCls = cls._env[cls._name];
    return new myCls(id );

    // To Be Check, 2019-5-2, Why return all ids?
    //return new myCls(id || Object.keys(cls._records));
  };


  cls.ref = async xmlid => {
    // get model and id from xmlid
    return cls._get_model('ir.model.data').call( 'xmlid_to_res_model_res_id', [xmlid, true] );
  };


  cls.search_count = async ({ domain }) => {
    const data0 = await cls.call( 'search_count', [domain] );

    return data0;
  };

  cls.default_get = async (fields_list ) => {
    const data = await cls.call( 'default_get', [fields_list] );
    return data;

  };

  return cls;
};

export default modelCreator;
